package com.ft.turorial.spring.boot.config;

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import com.ft.turorial.spring.boot.security.shiro.authc.credential.RetryLimitHashedCredentialsMatcher;
import com.ft.turorial.spring.boot.security.shiro.realm.UserRealm;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

/**
 * 集成Shiro核心内容:
 * 第一：ShiroFilterFactory，Shiro过滤器工程类，具体的实现类是：ShiroFilterFactoryBean，
 * 		此实现类是依赖于SecurityManager安全管理器。主要配置Filter就好
 * 
 * 第二：SecurityManager,Shiro的安全管理，主要是身份认证的管理，缓存管理，cookie管理，
 * 		所以在实际开发中我们主要是和SecurityManager进行打交道的，
 *
 * 第三：Realm,用于身份信息权限信息的验证.
 * 		重写两个方法:doGetAuthenticationInfo(获取即将需要认真的信息)、doGetAuthorizationInfo(获取通过认证后的权限信息)。
 * 
 * 第四：HashedCredentialsMatcher，凭证匹配器，用于告诉Shiro在认证时通过什么方式(算法)来匹配密码。
 * 
 * 第五：LifecycleBeanPostProcessor，Shiro生命周期处理器，保证实现了Shiro内部lifecycle函数的bean执行
 * 
 * 第六：开启Shiro的注解功能(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
 * 		需要配置两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)实现此功能
 * 
 * 第七：SpringTemplateEngine，用于Thymeleaf模板在页面上解析Shiro标签功能(如果使用jsp模板，就不需要此Bean)
 * 
 * 第八：其它的就是缓存管理，记住登录、验证码、分布式系统共享Session之类的，这些大部分都是需要自己进行的实现，
 * 		其中缓存管理，记住登录比较简单实现，并需要注入到SecurityManager让Shiro的安全管理器进行管理就好了。
 * 
 * 接着上一实例：补充缓存管理
 * 第九：Shiro缓存，步骤：
 * 		1.创建缓存管理器@Bean；
 * 		2.将缓存管理器注入到SecurityManager中；
 * 		3.在realm中开启缓存(默认是false)
 * 第十：Shiro记住我(rememberMe)，步骤：
 * 		1.创建Cookie(记住我)管理器@Bean；
 * 		2.将Cookie管理器注入到SecurityManager中；
 * 		3.当访问指定路径资源时，只需要记录我就能访问，则在ShiroFilterFactoryBean添加记住我过滤器:user
 * 		4.目前我们采用的是创建Token的方式来登陆：UsernamePasswordToken(用户名，密码，是否记住我)，在创建Token后，加上token.setRememberMe(true);
 * 		    例如：
 * 			if(request.getParameter("rememberMe")!=null){
 *	        	token.setRememberMe(true);
 *	        }
 *		    如果是采用FormAuthenticationFilter来认证的话，如果勾上了"记住我"，会自动帮我们设置上true
 * 接着上一实例：补充验证码功能
 * 第十一：在Shiro身份认证中，添加验证码认证，步骤：
 * 		1.扩展shiro的认证
 * 		  (1)创建验证码异常类：CaptchaException.java
 * 		  (2)扩展默认的用户认证的UsernamePasswordToken为：UsernamePasswordCaptchaToken.java
 * 		  (3)修改shiro认证逻辑(UserRealm.java)：增加验证码验证逻辑
 * 		2.验证码工具类 CaptchaUtil
 * 		3.创建验证码的servlet/filter,并注入 
 * 		4.在shiroFilter加上访问servlet时的权限为 anon
 * 		5.在SecurityController的登陆方法中，使用UsernamePasswordCaptchaToken来调用login方法
 * 		
 * 		补充增加内容：1.如果已经登陆，再访问login请求时， 跳转到指定页面，详细见login的 get请求方法
 * 					2.如果访问的资源需要登录认证(自动会跳转到登陆页面)，等登陆认证通过后，则重定向到刚才访问的页面
 * 
 * 接着上一实例：补充验证码功能
 * 第十二：在Shiro身份认证时，增加错误密码失败次数，超过指定次数(5次)时，自动锁定用户多长时间，步骤：
 * 		1.实现自定义匹配器RetryLimitHashedCredentialsMatcher，需要继承HashedCredentialsMatcher
 * 		2.在ehcache.xml中配置passwordRetryCache缓存参数【登录记录缓存 锁定10【timeToIdleSeconds=600】分钟】
 * 		3.在页面显示密码错误时，显示错误次数
 * 
 * 接着上一实例：补充验证码功能
 * 第十三：Shiro身份切换 、添加SessionUtil工具，实现全局Session缓存当前用户信息等
 * 	    全局缓存当前用户信息：
 * 		1.添加SessionUtil类
 * 		2.添加SecurityInterceptor拦截器，实现HandlerInterceptor接口，在preHandle方法中注入当前Session到SessionUtil类的 session thread中
 * 		3.在UserRealm获取认证信息方法中，通过SessionUtil.setUser(user);
 * 	  Shiro身份切换：
 * 		1.不需要修改配置，只需要添加一个身份授权表(两个字段：授权用户id(from_user_id)和被授权用户id(to_user_id))
 * 		2.实现接口(dao/service)：grantRunAs(授予身份，即save操作)、revokeRunAs(回收身份，即delete操作)
 * 		3.在Controller调用接口实现授予身份、回收身份、切换身份(调用subject.runAs()实现)
 * 
 * 
 * @author lc
 *
 */
@Configuration
public class ShiroConfiguration {
	private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);

	/**
	 * Shiro的Web过滤器Factory 命名:shiroFilter<br />
	 * 
	 * @param securityManager
	 * @return
	 */
	@Bean(name = "shiroFilter")
	public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
		logger.info("注入Shiro的Web过滤器-->shiroFilter", ShiroFilterFactoryBean.class);
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

		//Shiro的核心安全接口,这个属性是必须的
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		//要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
		shiroFilterFactoryBean.setLoginUrl("/login");
		//登录成功后要跳转的连接,逻辑也可以自定义，例如返回上次请求的页面
		shiroFilterFactoryBean.setSuccessUrl("/index");
		//用户访问未对其授权的资源时,所显示的连接
		shiroFilterFactoryBean.setUnauthorizedUrl("/pages/403");
		/*定义shiro过滤器,例如实现自定义的FormAuthenticationFilter，需要继承FormAuthenticationFilter
		**本例中赞不自定义实现，在下一节实现验证码的例子中体现
		*/
		
		/*定义shiro过滤链  Map结构
		 * Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
		 * anon：它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 
		 * authc：该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
		 */
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
		filterChainDefinitionMap.put("/logout", "logout");
	    //配置记住我过滤器或认证通过可以访问的地址(当上次登录时，记住我以后，在下次访问/或/index时，可以直接访问，不需要登陆)
	    filterChainDefinitionMap.put("/index", "user");
	    filterChainDefinitionMap.put("/", "user");
		//过滤链定义，从上向下顺序执行，一般将 /**放在最为下边 -->:这是一个坑呢，一不小心代码就不好使了;
		//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
		filterChainDefinitionMap.put("/webui/**", "anon");
		filterChainDefinitionMap.put("/webjars/**", "anon");
		filterChainDefinitionMap.put("/login", "anon");
		filterChainDefinitionMap.put("/login/captcha", "anon");
		filterChainDefinitionMap.put("/**", "authc");

		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		
		return shiroFilterFactoryBean;
	}

	/**
	 * 不指定名字的话，自动创建一个方法名第一个字母小写的bean
	 * @Bean(name = "securityManager")
	 * @return
	 */
	@Bean
	public SecurityManager securityManager() {
		logger.info("注入Shiro的Web过滤器-->securityManager", ShiroFilterFactoryBean.class);
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		//设置Realm，用于获取认证凭证
		securityManager.setRealm(userRealm());
		//注入缓存管理器
	    securityManager.setCacheManager(ehCacheManager());//这个如果执行多次，也是同样的一个对象;
	    //注入Cookie(记住我)管理器(remenberMeManager)
	    securityManager.setRememberMeManager(rememberMeManager());
	    
		return securityManager;
	}
	
	/**
	 * Shiro Realm 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的
	 * 
	 * @param cacheManager
	 * @return
	 */
	@Bean
	public UserRealm userRealm() {
		UserRealm userRealm = new UserRealm();
		//告诉realm,使用credentialsMatcher加密算法类来验证密文
		userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		//启用缓存,默认false
		userRealm.setCachingEnabled(true);
		//  启用身份验证缓存，即缓存AuthenticationInfo信息，默认false；
		userRealm.setAuthenticationCachingEnabled(true);
		//  缓存AuthenticationInfo信息的缓存名称,即配置在ehcache.xml中的cache name
		userRealm.setAuthenticationCacheName("authenticationCache");
		//  启用授权缓存，即缓存AuthorizationInfo信息，默认false；
		userRealm.setAuthorizationCachingEnabled(true);
		//  缓存AuthorizationInfo信息的缓存名称；
		userRealm.setAuthorizationCacheName("authorizationCache");
		
		return userRealm;
	}
	
	/**
     * 凭证匹配器
     * （由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     *  所以我们需要修改下doGetAuthenticationInfo中的代码;
     * ）
     * 可以扩展凭证匹配器，实现 输入密码错误次数后锁定等功能，下一次
     * @return
     */
    @Bean(name="credentialsMatcher")
    public RetryLimitHashedCredentialsMatcher hashedCredentialsMatcher(){
    	RetryLimitHashedCredentialsMatcher hashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(ehCacheManager());
      
       hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
       hashedCredentialsMatcher.setHashIterations(2);//散列的次数，比如散列两次，相当于 md5(md5(""));
       //storedCredentialsHexEncoded默认是true，此时用的是密码加密用的是Hex编码；false时用Base64编码
       hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
       
       return hashedCredentialsMatcher;
    }
    /**
	 * Shiro CacheManager 定义 Shiro 缓存管理器
	 * 
	 * 需要加入到安全管理器：securityManager
	 * @return
	 */
	@Bean
	public EhCacheManager ehCacheManager() {
		logger.info("注入Shiro的缓存管理器-->ehCacheManager", EhCacheManager.class);
		EhCacheManager cacheManager = new EhCacheManager();
		cacheManager.setCacheManagerConfigFile("classpath:META-INF/ehcache/ehcache.xml");
		return cacheManager;
	}
	
	/**
	 * cookie管理器;
	 * @return
	 */
	@Bean
	public CookieRememberMeManager rememberMeManager(){
		logger.info("注入Shiro的记住我(CookieRememberMeManager)管理器-->rememberMeManager", CookieRememberMeManager.class);
		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
		//rememberme cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度（128 256 512 位），通过以下代码可以获取
		//KeyGenerator keygen = KeyGenerator.getInstance("AES");
		//SecretKey deskey = keygen.generateKey();
		//System.out.println(Base64.encodeToString(deskey.getEncoded()));
		byte[] cipherKey = Base64.decode("wGiHplamyXlVB11UXWol8g==");
		cookieRememberMeManager.setCipherKey(cipherKey);
	    cookieRememberMeManager.setCookie(rememberMeCookie());
		return cookieRememberMeManager;
	}
	@Bean
	public SimpleCookie rememberMeCookie(){
		//这个参数是cookie的名称，对应前端的checkbox的name = rememberMe
	    SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
	    //如果httyOnly设置为true，则客户端不会暴露给客户端脚本代码，使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击；
	    simpleCookie.setHttpOnly(true);
	    //记住我cookie生效时间,默认30天 ,单位秒：60 * 60 * 24 * 30
	    simpleCookie.setMaxAge(259200);
	    
	    return simpleCookie;
	}
    /**
     * Shiro生命周期处理器
     * @return
     */
    /*@Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
    	return new LifecycleBeanPostProcessor();
    }*/
    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    /*@Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
    	DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    	advisorAutoProxyCreator.setProxyTargetClass(true);
    	return advisorAutoProxyCreator;
    }*/
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
    	AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    	authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
    	return authorizationAttributeSourceAdvisor;
    }
    
    /**
     * 注解thymeleaf集成的Spring模板，将Shiro的标签库加入到thymeleaf的模板解析中
     * @param templateResolver
     * @return
     */
    /*@Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
		SpringTemplateEngine templateEngine = new SpringTemplateEngine();
		templateEngine.setTemplateResolver(templateResolver);
		Set<IDialect> additionalDialects = new HashSet<IDialect>();
		additionalDialects.add(new ShiroDialect());
		templateEngine.setAdditionalDialects(additionalDialects);
		return templateEngine;
	}*/
    /**
     * 添加ShiroDialect 为了在thymeleaf里使用shiro的标签的bean
     * 跟templateEngine方法功能一样
     * @param templateResolver
     * @return
     */
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect(){
      return new ShiroDialect();
    }
}
